package com.dbflow5.query;

import com.dbflow5.config.FlowManager;
import com.dbflow5.query.Join.JoinType;
import com.dbflow5.query.property.IndexProperty;
import com.dbflow5.sql.Query;
import com.dbflow5.structure.ChangeAction;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;

/**
 * Description: The SQL FROM query wrapper that must have a [Query] base.
 */
public class From<TModel> extends BaseTransformable<TModel>{

    private final Query queryBuilderBase;
    /**
     * If specified, we use this as the subquery for the FROM statement.
     */
    private final ModelQueriable<TModel> modelQueriable;

    /**
     * An alias for the table
     */
    private NameAlias tableAlias = null;

    /**
     * Enables the SQL JOIN statement
     */
    private final List<Join<?, ?>> joins = new ArrayList<>();

    public From(Query queryBuilderBase, Class<TModel> table, ModelQueriable<TModel> modelQueriable){
        super(table);
        this.queryBuilderBase = queryBuilderBase;
        this.modelQueriable = modelQueriable;
    }

    @Override
    public Query queryBuilderBase() {
        return queryBuilderBase;
    }

    @Override
    public ChangeAction primaryAction() {
        return queryBuilderBase instanceof Delete? ChangeAction.DELETE : ChangeAction.CHANGE;
    }

    @Override
    public String getQuery() {
        StringBuilder queryBuilder = new StringBuilder();
        queryBuilder.append(queryBuilderBase.getQuery());
        if (!(queryBuilderBase instanceof Update<?>)) {
            queryBuilder.append("FROM ");
        }

        if(modelQueriable != null){
            queryBuilder.append(modelQueriable.enclosedQuery());
        }else {
            queryBuilder.append(getTableAlias().toString());
        }

        if (queryBuilderBase instanceof Select) {
            if (!joins.isEmpty()) {
                queryBuilder.append(" ");
            }
            joins.forEach(join -> queryBuilder.append(join.getQuery()));
        } else {
            queryBuilder.append(" ");
        }

        System.out.println(queryBuilder);
        return queryBuilder.toString();
    }

    @Override
    public From<TModel> cloneSelf() {
        From<TModel> from = new From<>(queryBuilderBase instanceof Select? ((Select) queryBuilderBase).cloneSelf():queryBuilderBase,
                table, null);
        from.joins.addAll(joins);
        from.tableAlias = tableAlias;
        return from;
    }

    /**
     * tables
     *
     * @return A list of [Class] that represents tables represented in this query. For every
     * [Join] on another table, this adds another [Class].
     *
     * @return tables
     */
    public Set<Class<?>> associatedTables() {
        LinkedHashSet<Class<?>> tables = new LinkedHashSet<>(Collections.singleton(table));
        for (Join<?, ?> join : joins) {
            tables.add(join.table);
        }
        return tables;
    }

    private NameAlias getTableAlias() {
        if(tableAlias == null){
            tableAlias = new NameAlias.Builder(FlowManager.getTableName(table)).build();
        }
        return tableAlias;
    }

    /**
     * Set an alias to the table name of this [From].
     *
     * @param alias alias
     * @return this
     */
    public From<TModel> as(String alias) {
        tableAlias = getTableAlias()
                .newBuilder()
                .as(alias)
                .build();
        return this;
    }

    /**
     * Adds a join on a specific table for this query
     *
     * @param table    The table this corresponds to
     * @param joinType The type of join to use
     * @param <TJoin> TJoin
     * @return join
     */
    public <TJoin> Join<TJoin, TModel> join(Class<TJoin> table, JoinType joinType) {
        Join<TJoin, TModel> join = new Join<>(this, table, joinType);
        joins.add(join);
        return join;
    }

    /**
     * Adds a join on a specific table for this query.
     *
     * @param modelQueriable A query we construct the [Join] from.
     * @param joinType       The type of join to use.
     * @param <TJoin> TJoin
     * @return join
     */
    public <TJoin> Join<TJoin, TModel> join(ModelQueriable<TJoin> modelQueriable, JoinType joinType) {
        Join<TJoin, TModel> join = new Join<>(this, joinType, modelQueriable);
        joins.add(join);
        return join;
    }

    /**
     * Adds a [JoinType.CROSS] join on a specific table for this query.
     *
     * @param table   The table to join on.
     * @param <TJoin> The class of the join table.
     * @return join(table, JoinType.CROSS)
     */
    public <TJoin> Join<TJoin, TModel> crossJoin(Class<TJoin> table) {
        return join(table, JoinType.CROSS);
    }

    /**
     * Adds a [JoinType.CROSS] join on a specific table for this query.
     *
     * @param modelQueriable The query to join on.
     * @param <TJoin>        The class of the join table.
     * @return join(modelQueriable, JoinType.CROSS)
     */
    public <TJoin> Join<TJoin, TModel> crossJoin(ModelQueriable<TJoin> modelQueriable) {
        return join(modelQueriable, JoinType.CROSS);
    }

    /**
     * Adds a [JoinType.INNER] join on a specific table for this query.
     *
     * @param table   The table to join on.
     * @param <TJoin> The class of the join table.
     * @return join(table, JoinType.INNER)
     */
    public <TJoin> Join<TJoin, TModel> innerJoin(Class<TJoin> table) {
        return join(table, JoinType.INNER);
    }

    /**
     * Adds a [JoinType.INNER] join on a specific table for this query.
     *
     * @param modelQueriable The query to join on.
     * @param <TJoin>        The class of the join table.
     * @return join(modelQueriable, JoinType.INNER);
     */
    public <TJoin> Join<TJoin, TModel> innerJoin(ModelQueriable<TJoin> modelQueriable) {
        return join(modelQueriable, JoinType.INNER);
    }

    /**
     * Adds a [JoinType.LEFT_OUTER] join on a specific table for this query.
     *
     * @param table   The table to join on.
     * @param <TJoin> The class of the join table.
     * @return join(table, JoinType.LEFT_OUTER)
     */
    public <TJoin> Join<TJoin, TModel> leftOuterJoin(Class<TJoin> table) {
        return join(table, JoinType.LEFT_OUTER);
    }

    /**
     * Adds a [JoinType.LEFT_OUTER] join on a specific table for this query.
     *
     * @param modelQueriable The query to join on.
     * @param <TJoin>        The class of the join table.
     * @return join(modelQueriable, JoinType.LEFT_OUTER)
     */
    public <TJoin> Join<TJoin, TModel> leftOuterJoin(ModelQueriable<TJoin> modelQueriable) {
        return join(modelQueriable, JoinType.LEFT_OUTER);
    }

    /**
     * Adds a [JoinType.NATURAL] join on a specific table for this query.
     *
     * @param table   The table to join on.
     * @param <TJoin> The class of the join table.
     * @return join(table, JoinType.NATURAL)
     */
    public <TJoin> Join<TJoin, TModel> naturalJoin(Class<TJoin> table) {
        return join(table, JoinType.NATURAL);
    }

    /**
     * Adds a [JoinType.NATURAL] join on a specific table for this query.
     *
     * @param modelQueriable The query to join on.
     * @param <TJoin> The class of the join table.
     * @return join(modelQueriable, JoinType.NATURAL)
     */
    public <TJoin> Join<TJoin, TModel> naturalJoin(ModelQueriable<TJoin> modelQueriable) {
        return join(modelQueriable, JoinType.NATURAL);
    }

    /**
     * Begins an INDEXED BY piece of this query with the specified name.
     *
     * @param indexProperty The index property generated.
     * @return IndexedBy
     */
    public IndexedBy<TModel> indexedBy(IndexProperty<TModel> indexProperty) {
        return new IndexedBy<>(indexProperty, this);
    }

    @Override
    public Class<TModel> table() {
        return super.table();
    }
}

